package dava.csvgenerator;


import de.odysseus.el.ExpressionFactoryImpl;
import de.odysseus.el.TreeMethodExpression;
import de.odysseus.el.util.SimpleContext;
import java.io.IOException;
import java.io.InputStream;
import java.text.DateFormat;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.ArrayList;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import javax.el.ELException;
import javax.el.PropertyNotFoundException;
import javax.enterprise.inject.Any;
import javax.enterprise.inject.Instance;
import javax.inject.Inject;
import javax.xml.XMLConstants;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.xml.sax.SAXException;


/**
 * Class Controller controls the process of generating CSV file according to instance of
 * InputStream, which contains the configuration of the content and view for the CSV file.
 *
 * @author dava
 */
public class Controller {

    /**
     * All instances with annotation @CsvBean are injected here.
     */
    @Any
    @Inject
    @CsvBean
    private Instance<Object> fieldClasses;

    /**
     * Log handler.
     */
    private static final Log LOG = LogFactory.getLog(Controller.class);

    /**
     * Method for evaluating EL expressions.
     *
     * @param el EL expression for evaluation
     * @param context Context information for expression evaluation
     * @return Result of evaluated EL expression
     * @throws CsvEvalException On any error in the evaluation expressions.
     */
    private Object getObject(final String el, final SimpleContext context) throws CsvEvalException {
        ExpressionFactoryImpl factory = new ExpressionFactoryImpl();

        if (!el.startsWith("#{") && !el.startsWith("${")) {
            throw new CsvEvalException("EL expression is invalid: " + el);
        }

        String firstChar = el.substring(0, 2);
        String[] els = el.split("\\.");
        if (els.length > 2) {
            String expression = els[0] + "." + els[1] + "}";
            this.setContext(context, "o2", this.getObject(expression, context));
            for (int i = 2; i < els.length; i++) {
                String contextName = "o" + i;
                expression = firstChar + contextName + "." + els[i];
                if (i < els.length - 1) {
                    expression = firstChar + contextName + "." + els[i] + "}";
                    contextName = "o" + (i + 1);
                    this.setContext(context, contextName, this.getObject(expression, context));
                } else {
                    expression = firstChar + contextName + "." + els[i];
                    return this.getObject(expression, context);
                }
            }
        }

        try {
            try {
                TreeMethodExpression e;
                if (el.contains("(")) {
                    e = factory.createMethodExpression(context, el, null, null);
                } else {
                    e = factory.createMethodExpression(context, el, null, new Class<?>[]{});
                }
                return e.invoke(context, null);
            } catch (NoSuchMethodError e) {
                throw new CsvEvalException("Error in evaluating the EL expression: "
                        + "Method not found", e);
            } catch (PropertyNotFoundException e) {
                throw new CsvEvalException("Error in evaluating the EL expression: "
                        + "Property not found", e);
            } catch (ELException e) {
                throw new CsvEvalException("Error in evaluating the EL expression", e);
            }
        } catch (CsvEvalException ex) {
            try {
                String objectName = el.subSequence(2, el.indexOf(".")).toString();
                for (Object o : this.fieldClasses) {
                    if (o.getClass().getAnnotation(CsvBean.class).name().equals(objectName)) {
                        context.setVariable(objectName,
                                factory.createValueExpression(o, o.getClass()));
                    }
                }
                TreeMethodExpression e;
                if (el.contains("(")) {
                    e = factory.createMethodExpression(context, el, null, null);
                } else {
                    e = factory.createMethodExpression(context, el, null, new Class<?>[]{});
                }
                return e.invoke(context, null);
            } catch (NoSuchMethodError e) {
                throw new CsvEvalException("Error in evaluating the EL expression: "
                        + "Method not found", e);
            } catch (PropertyNotFoundException e) {
                throw new CsvEvalException("Error in evaluating the EL expression: "
                        + "Property not found", e);
            } catch (ELException e) {
                throw new CsvEvalException("Error in evaluating the EL expression", e);
            }
        }
    }

    /**
     * Method for creating a CSV files according to input configuration
     *
     * @param is InputStream which is XML document with configuration for creating a CSV files
     * @param object Instance of the required information which can be generated in CSV file.
     * @return List of CSV files
     * @throws CsvEvalException On any error in the processing of input data and writing
     * output CSV file.
     */
    public List<OutputCsv> resolve(InputStream is, Object object) throws CsvEvalException {
        List<OutputCsv> outputs = new ArrayList<OutputCsv>();
        SimpleContext context = new SimpleContext();
        this.setContext(context, "bases", object);

        try {
            final Schema schema = SchemaFactory.newInstance(
                    XMLConstants.W3C_XML_SCHEMA_NS_URI).newSchema(
                    Controller.class.getResource("CSV_schema.xsd"));
            final Unmarshaller unmarshaller = JAXBContext.newInstance(
                    PackageCsv.class).createUnmarshaller();
            unmarshaller.setSchema(schema);
            PackageCsv topElem = (PackageCsv) unmarshaller.unmarshal(is);

            for (CsvType csv : topElem.getCsv()) {
                outputs.add(this.createCsv(topElem, csv, context));
            }
            if (topElem.getForEachCSV() != null) {
                Iterable<?> csvObjects = null;
                Object obj = this.getObject(topElem.getForEachCSV().getItems(), context);
                if (obj instanceof Iterable) {
                    csvObjects = (Iterable) obj;
                } else {
                    throw new CsvEvalException(String.format("Error in the processing of input"
                            + " data: Result of EL expression %s must be iterable",
                            topElem.getForEachCSV().getItems()));
                }
                for (Object csvObject : csvObjects) {
                    this.setContext(context, topElem.getForEachCSV().getVar(), csvObject);
                    outputs.add(this.createCsv(topElem, topElem.getForEachCSV().getCsv(), context));
                }
            }
        } catch (SAXException e) {
            throw new CsvEvalException("Error in the processing of input data", e);
        } catch (JAXBException e) {
            throw new CsvEvalException("Error in the processing of input data: JAXB processing", e);
        }
        return outputs;
    }

    /**
     * Method for creating one CSV file.
     *
     * @param topElem Top element of the input XML document
     * @param csv Instance representing an element <csv> of the input XML document
     * @return context Context information for expression evaluation
     * @throws CsvEvalException On any error in the processing of input data and writing
     * output CSV file.
     */
    private OutputCsv createCsv(PackageCsv topElem, CsvType csv, SimpleContext context)
            throws CsvEvalException {
        OutputCsv output = null;
        if (isEL(csv.getName())) {
            output = new OutputCsv(this.getObject(csv.getName(), context).toString());
        } else {
            output = new OutputCsv(csv.getName());
        }
        try {
            final String encoding = csv.getEncoding().value();
            if (csv.getHeadingRow() != null) {
                if (csv.getHeadingRow().getHeadingString() != null) {
                    if (this.isEL(csv.getHeadingRow().getHeadingString())) {
                        output.write(this.getObject(csv.getHeadingRow().getHeadingString(),
                                context).toString().getBytes(encoding));
                        output.write(this.newLine(csv.getNewLine()).getBytes(encoding));
                    } else {
                        output.write(csv.getHeadingRow().getHeadingString().getBytes(encoding));
                        output.write(this.newLine(csv.getNewLine()).getBytes(encoding));
                    }
                    if (csv.getHeadingRow().getColumn() != null) {
                        LOG.debug(String.format("XML for CSV file with name %s:"
                                + "Columns from the element headingRow not fill,"
                                + " because element headingRow has set an attribute,"
                                + " which has a higher priority ", csv.getName()));
                    }
                } else if (csv.getHeadingRow().getColumn() != null) {
                    this.fillRow(csv, csv.getHeadingRow().getColumn(), context, output, encoding);
                }
            }
            for (ForEachCalculationType calType : csv.getForEachCalculation()) {
                Iterable<?> calObjects = null;
                Object calObj = this.getObject(calType.getItems(), context);
                if (calObj instanceof Iterable) {
                    calObjects = (Iterable) calObj;
                } else {
                    throw new CsvEvalException(String.format("Error in the processing of input"
                            + " data: Result of EL expression %s must be iterable",
                            calType.getItems()));
                }
                for (Object calObject : calObjects) {
                    this.setContext(context, calType.getVar(), calObject);
                    for (RowType row : calType.getRow()) {
                        this.fillRow(csv, row.getColumn(), context, output, encoding);
                    }
                }
            }
        } catch (IOException e) {
            throw new CsvEvalException("Error when writing output CSV file", e);
        } finally {
            try {
                output.flush();
                output.close();
            } catch (IOException e) {
                throw new CsvEvalException("Error when writing output CSV file", e);
            }
        }
        return output;
    }

    /**
     * Method for fill one row of CSV file.
     *
     * @param csv Instance representing an element <csv> of the input XML document
     * @param columns List of columns of the CSV file
     * @param context Context information for expression evaluation
     * @param output OutputCSV for writing data
     * @param encoding Encoding for writing output data
     * @throws CsvEvalException On any error in the processing of input data and writing
     * output CSV file.
     */
    private void fillRow(CsvType csv, List<ColumnType> columns, SimpleContext context,
            OutputCsv output, String encoding) throws CsvEvalException {
        int numberOfColumn = 0;
        for (ColumnType column : columns) {
            numberOfColumn++;
            String columnText = "";
            for (OutputType out : column.getOutput()) {
                if (this.isEL(out.getValue())) {
                    Object object = this.getObject(out.getValue(), context);
                    if (object instanceof Number) {
                        columnText = columnText.concat(NumberFormat.getNumberInstance(
                                new Locale(csv.getLocale())).format(object));
                    } else if (object instanceof Date) {
                        if (column.getFormatDate() != null) {
                            try {
                                DateFormat dateFormater = new SimpleDateFormat(
                                        column.getFormatDate(), new Locale(csv.getLocale()));
                                dateFormater.setTimeZone(TimeZone.getTimeZone(csv.getTimeZone()));
                                columnText = columnText.concat(dateFormater.format(object));
                            } catch (IllegalArgumentException e) {
                                throw new CsvEvalException("Error in the processing of input data:"
                                        + " Date format of any columns is invalid", e);
                            }
                        } else if (csv.getFormatDate() != null) {
                            try {
                                DateFormat dateFormater = new SimpleDateFormat(csv.getFormatDate(),
                                        new Locale(csv.getLocale()));
                                dateFormater.setTimeZone(TimeZone.getTimeZone(csv.getTimeZone()));
                                columnText = columnText.concat(dateFormater.format(object));
                            } catch (IllegalArgumentException e) {
                                throw new CsvEvalException("Error in the processing of input data:"
                                        + " The set date format for the CSV file is invalid", e);
                            }
                        } else {
                            DateFormat dateFormater = DateFormat.getDateTimeInstance(
                                    DateFormat.MEDIUM, DateFormat.MEDIUM,
                                    new Locale(csv.getLocale()));
                            dateFormater.setTimeZone(TimeZone.getTimeZone(csv.getTimeZone()));
                            columnText = columnText.concat(dateFormater.format(object));
                        }
                    } else if (object != null) {
                        columnText = columnText.concat(object.toString());
                    }
                } else {
                    columnText = columnText.concat(out.getValue());
                }
            }
            if (columnText.contains(csv.getQuota())) {
                columnText = columnText.replace(csv.getQuota(), csv.getQuota() + csv.getQuota());
            }
            if (column.isUsingQuote() || columnText.contains(csv.getSeparator())
                    || columnText.contains(csv.getLineSeparator())
                    || columnText.contains(csv.getQuota())) {
                columnText = csv.getQuota() + columnText + csv.getQuota();
            }
            try {
                if (column.getColumnWidth() != null
                        && column.getColumnWidth() > columnText.length()) {
                    if (column.getAlign() != null && column.getAlign().equals(AlignType.LEFT)) {
                        output.write(String.format("%-" + column.getColumnWidth() + "."
                                + column.getColumnWidth() + "s", columnText).getBytes(encoding));
                    } else {
                        output.write(String.format("%" + column.getColumnWidth() + "."
                                + column.getColumnWidth() + "s", columnText).getBytes(encoding));
                    }
                } else {
                    output.write(columnText.getBytes());
                }
                if (numberOfColumn != columns.size()) {
                    output.write(csv.getSeparator().getBytes(encoding));
                }
            } catch (IOException e) {
                throw new CsvEvalException("Error when writing output CSV file", e);
            }
        }
        try {
            if (csv.getLineSeparator() != null) {
                output.write(csv.getLineSeparator().getBytes(encoding));
            }
            output.write(this.newLine(csv.getNewLine()).getBytes(encoding));
        } catch (IOException e) {
            throw new CsvEvalException("Error when writing output CSV file", e);
        }
    }

    private void setContext(SimpleContext context, String string, Object o) {
        ExpressionFactoryImpl factory = new ExpressionFactoryImpl();
        context.setVariable(string, factory.createValueExpression(o, o.getClass()));
    }

    private String newLine(NewLineType type) {
        if (type.equals(NewLineType.WINDOWS)) {
            return "\r\n";
        } else if (type.equals(NewLineType.LINUX)) {
            return "\n";
        } else {
            return null;
        }
    }

    private boolean isEL(String el) {
        if (!el.startsWith("#{") && !el.startsWith("${")) {
            return false;
        } else {
            return true;
        }
    }

}
